/**
 * 
 */
package com.cfw.xml;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import com.cfw.properties.PropertiesHolder;
import com.cfw.util.XmlUtil;

/**
 * XML 리소스들을 통합적으로 관리하기 위한 Utility Class<br/>
 * <br/><b>History</b><br/>
 * <pre>
 * 2012. 4. 19. 최초작성
 * </pre>
 * @author 박형일
 * @version 1.0
 */
public final class XmlManager {
	private static final Logger logger = LoggerFactory.getLogger(XmlManager.class); 
	
	private final Map<String, Document> documents =	new HashMap<String, Document>();
	private static final XmlManager instance = new XmlManager();
	
	/**
	 * singleton이기 때문에 초기화 작업 수행 후 XML작업은 수행하지 않는다.
	 */
	private XmlManager() { }
	
	public static XmlManager getInstance() { return instance; }

	/**
	 * xmlId를 구분자로 갖는 document를 반환 
	 * @param xmlId
	 * @return
	 */
	public Document getDocuments(final String xmlId) { return documents.get(xmlId); }
	
	/**
	 * document를 documents field에 추가
	 * @param documentName
	 * @param document
	 */
	private void setDocuments(final String documentName, final Document document) { documents.put(documentName, document); }
	
	/**
	 * filename경로의 xml화일을 로딩하여 documents field에 추가
	 * @param fileName
	 * @throws ParserConfigurationException
	 * @throws IOException
	 * @throws SAXException
	 */
	public void loadXml(final String fileName) throws ParserConfigurationException, IOException, SAXException {
		if (StringUtils.isEmpty(fileName)) {
			throw new IllegalArgumentException("매개변수 파일명이 null이거나 빈문자열입니다.");
		}

		logger.debug("document[" + fileName + "]를 로딩합니다.");
		
		final File file = new File(fileName);
		final String xmlName = FilenameUtils.getName(fileName);
		final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		
		synchronized(this) {
			setDocuments(xmlName, builder.parse(file));
		}
	}

	/**
	 * resourceName경로의 resource를 로딩하여 documents field에 추가
	 * @param resourceName
	 * @throws ParserConfigurationException
	 * @throws IOException
	 * @throws SAXException
	 */
	public String loadXmlStream(final String resourceName) throws ParserConfigurationException, IOException, SAXException {
		if (StringUtils.isEmpty(resourceName)) {
			throw new IllegalArgumentException("매개변수 리소스명이 null이거나 빈문자열입니다.");
		}

		logger.debug("document[" + resourceName + "]를 로딩합니다.");
		
		final String xmlName = FilenameUtils.getName(resourceName);
		final InputStream stream = XmlManager.class.getResourceAsStream(resourceName);
		final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
		
		synchronized(this) {
			setDocuments(xmlName, builder.parse(stream));
		}
		
		return xmlName;
	}
	
		/**
		 * attributeName을 attribute name으로 하는 xpath 검색 문자열 반환
		 * @param attributeName
		 * @param attributeValue
		 * @return
		 */
		private String getSearchExpression(final String attributeName, final String attributeValue) {
			return "//*[@" + attributeName + "='" + attributeValue + "']";
		}
		
		/**
		 * xmlId를 구분자로 갖는 document에서 xpath 검색 문자열인 searchExpression으로 검색을 수행한 결과 NodeList를 반환
		 * @param xmlId
		 * @param searchExpression
		 * @return
		 * @throws XPathExpressionException
		 * @throws IllegalArgumentException
		 */
		private NodeList getNodeListForSearch(final String xmlId, final String searchExpression) throws XPathExpressionException, IllegalArgumentException {
			if (StringUtils.isEmpty(xmlId) || StringUtils.isEmpty(searchExpression)) {
				throw new IllegalArgumentException("매개변수 중의 하나 이상이 null이거나 빈문자열입니다.");
			}
			
			final Document document = getDocuments(xmlId);
			
			if (document == null) {
				throw new UnsupportedOperationException("XML객체를 찾을 수 없습니다.");
			}
			
			final XPath xpath = XPathFactory.newInstance().newXPath();
			final NodeList nodeList = (NodeList)xpath.evaluate(searchExpression, document, XPathConstants.NODESET);
			
			return nodeList;
		}		
	
	/**
	 * xmlId를 구분자로 갖는 document에서 xpath 검색 문자열인 searchExpression으로 검색을 수행한 결과를 Map으로 반환<br/>
	 * 검색된 결과 NodeList에는 1개의 Node가 포함되어야 하며, 1개 이상일 경우 오류 발생
	 * @param xmlId
	 * @param searchExpression
	 * @return
	 * @throws XPathExpressionException
	 */
	public Map<String,Object> getSingleNode(final String xmlId, final String searchExpression) throws XPathExpressionException {
		final List<Map<String,Object>> list = getNode(xmlId, searchExpression);
		HashMap<String,Object> hashMap = null;
		
		if (list.size() == 1) {
			hashMap = (HashMap<String,Object>)list.get(0);
		} else {
			throw new UnsupportedOperationException("결과 노드가 존재하지 않거나 1개 이상입니다.");
		}

		return hashMap;
	}
	
	/**
	 * xmlId를 구분자로 갖는 document에서 attributeName attribute의 값이 attributeValue인 검색을 수행한 결과를 Map으로 반환<br/>
	 * 검색된 결과 NodeList에는 1개의 Node가 포함되어야 하며, 1개 이상일 경우 오류 발생
	 * @param xmlId
	 * @param attributeName
	 * @param attributeValue
	 * @return
	 * @throws XPathExpressionException
	 */
	public Map<String,Object> getSingleNode(final String xmlId, final String attributeName, final String attributeValue) throws XPathExpressionException {
		final String searchExpression =	getSearchExpression(attributeName, attributeValue);
		
		return getSingleNode(xmlId, searchExpression);
	}
	
	/**
	 * xmlId를 구분자로 갖는 document에서 attributeName attribute의 값이 attributeValue인 검색을 수행한 결과를 List로 반환
	 * @param xmlId
	 * @param attributeName
	 * @param attributeValue
	 * @return
	 * @throws XPathExpressionException
	 */
	public List<Map<String,Object>> getNode(final String xmlId, final String attributeName, final String attributeValue) throws XPathExpressionException {
		final String searchExpression =	getSearchExpression(attributeName, attributeValue);
		
		return getNode(xmlId, searchExpression);
	}
	
	/**
	 * xmlId를 구분자로 갖는 document에서 xpath 검색 문자열인 searchExpression으로 검색을 수행한 결과를 List로 반환
	 * @param xmlId
	 * @param searchExpression
	 * @return
	 * @throws XPathExpressionException
	 */
	public List<Map<String,Object>> getNode(final String xmlId, final String searchExpression) throws XPathExpressionException {
		final NodeList nodelist = getNodeListForSearch(xmlId, searchExpression);
		
		final Map<String,Object> map = XmlUtil.nodeListToMap(nodelist);
		
		return (List<Map<String,Object>>)map.get("root");
	}

	/**
	 * xmlId를 구분자로 갖는 document에서 attributeName attribute의 값이 attributeValue인 검색을 수행한 결과의 하위 node들을 List로 반환
	 * @param xmlId
	 * @param attributeName
	 * @param attributeValue
	 * @return
	 * @throws XPathExpressionException
	 */
	public List<Map<String,Object>> getSubNode(final String xmlId, final String attributeName, final String attributeValue) throws XPathExpressionException {
		final String searchExpression =	getSearchExpression(attributeName, attributeValue);
		
		return getSubNode(xmlId, searchExpression);
	}		
		
	/**
	 * xmlId를 구분자로 갖는 document에서 xpath 검색 문자열인 searchExpression으로 검색을 수행한 결과의 하위 node들을 List로 반환
	 * @param xmlId
	 * @param searchExpression
	 * @return
	 * @throws XPathExpressionException
	 */
	public List<Map<String,Object>> getSubNode(final String xmlId, final String searchExpression) throws XPathExpressionException {
		final NodeList nodelist =	getNodeListForSearch(xmlId, searchExpression);
		
		if (nodelist.getLength() != 1) {
			throw new UnsupportedOperationException("결과 노드가 존재하지 않거나 1개 이상입니다.");
		}

		final Map<String, Object> map = XmlUtil.nodeToMap(nodelist.item(0));
		
		return (List<Map<String,Object>>)map.get("child");
	}
}
